package com.atguigu.gmall.list.service.impl;

import com.alibaba.fastjson.JSON;
import com.atguigu.gmall.list.model.*;
import com.atguigu.gmall.list.service.SearchService;
import com.atguigu.gmall.product.client.ProductFeignClient;
import com.atguigu.gmall.product.model.BaseAttrInfo;
import com.atguigu.gmall.product.model.BaseCategoryView;
import com.atguigu.gmall.product.model.BaseTrademark;
import com.atguigu.gmall.product.model.SkuInfo;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.apache.lucene.search.join.ScoreMode;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.action.update.UpdateRequest;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.*;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.bucket.nested.NestedAggregationBuilder;
import org.elasticsearch.search.aggregations.bucket.nested.ParsedNested;
import org.elasticsearch.search.aggregations.bucket.terms.ParsedLongTerms;
import org.elasticsearch.search.aggregations.bucket.terms.ParsedStringTerms;
import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.elasticsearch.search.sort.SortOrder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.io.IOException;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.stream.Collectors;

/**
 * @author: atguigu
 * @create: 2023-06-16 14:23
 */
@Slf4j
@Service
@SuppressWarnings("all")
public class SearchServiceImpl implements SearchService {
    private static final String index_name = "goods";


    @Autowired
    private ProductFeignClient productFeignClient;

    @Autowired
    private RestHighLevelClient restHighLevelClient;

    //传统方式需要每个类中生成日志对象,现在使用lombok注解替换
    //Logger logger = LoggerFactory.getLogger(SearchServiceImpl.class);

    @Autowired
    private ThreadPoolExecutor threadPoolExecutor;

    @Autowired
    private RedisTemplate redisTemplate;


    /**
     * 将指定SKU商品导入索引库
     *
     * @param skuId
     */
    @Override
    public void upperGoods(Long skuId) {
        try {
            //1.创建索引库文档对象Goods 为对象中属性赋值
            Goods goods = new Goods();
            //2.远程调用商品服务获取商品基本信息 优选从缓存中获取
            CompletableFuture<SkuInfo> skuInfoCompletableFuture = CompletableFuture.supplyAsync(() -> {
                SkuInfo skuInfo = productFeignClient.getSkuInfo(skuId);
                if (skuInfo != null) {
                    goods.setTitle(skuInfo.getSkuName());
                    goods.setId(skuId);
                    goods.setCreatedDate(skuInfo.getCreateTime());
                    goods.setCreateTime(skuInfo.getCreateTime());
                    goods.setDefaultImg(skuInfo.getSkuDefaultImg());
                }
                return skuInfo;
            }, threadPoolExecutor);
            //价格获取最新
            CompletableFuture<Void> priceCompletableFuture = CompletableFuture.runAsync(() -> {
                BigDecimal skuPrice = productFeignClient.getSkuPrice(skuId);
                if (skuPrice != null) {
                    goods.setPrice(skuPrice.doubleValue());
                }
            }, threadPoolExecutor);
            //3.远程调用商品服务获取品牌信息
            CompletableFuture<Void> tradeMarkCompletableFuture = skuInfoCompletableFuture.thenAcceptAsync(skuInfo -> {
                BaseTrademark trademark = productFeignClient.getBaseTrademarkById(skuInfo.getTmId());
                if (trademark != null) {
                    goods.setTmId(trademark.getId());
                    goods.setTmName(trademark.getTmName());
                    goods.setTmLogoUrl(trademark.getLogoUrl());
                }
            }, threadPoolExecutor);
            //4.远程调用商品服务获取品分类信息
            CompletableFuture<Void> categoryCompletableFuture = skuInfoCompletableFuture.thenAcceptAsync((skuInfo -> {
                BaseCategoryView categoryView = productFeignClient.getCategoryView(skuInfo.getCategory3Id());
                if (categoryView != null) {
                    goods.setCategory1Id(categoryView.getCategory1Id());
                    goods.setCategory1Name(categoryView.getCategory1Name());
                    goods.setCategory2Id(categoryView.getCategory2Id());
                    goods.setCategory2Name(categoryView.getCategory2Name());
                    goods.setCategory3Id(categoryView.getCategory3Id());
                    goods.setCategory3Name(categoryView.getCategory3Name());
                }
            }), threadPoolExecutor);

            //5.远程调用商品服务获取品平台属性列表信息
            CompletableFuture<Void> baseAttrCompletableFuture = CompletableFuture.runAsync(() -> {
                List<BaseAttrInfo> attrList = productFeignClient.getAttrList(skuId);
                if (!CollectionUtils.isEmpty(attrList)) {
                    List<SearchAttr> searchAttrList = attrList.stream().map(baseAttrInfo -> {
                        SearchAttr searchAttr = new SearchAttr();
                        searchAttr.setAttrId(baseAttrInfo.getId());
                        searchAttr.setAttrName(baseAttrInfo.getAttrName());
                        searchAttr.setAttrValue(baseAttrInfo.getAttrValue());
                        return searchAttr;
                    }).collect(Collectors.toList());
                    goods.setAttrs(searchAttrList);
                }
            }, threadPoolExecutor);


            //组合异步任务
            CompletableFuture.allOf(
                    skuInfoCompletableFuture,
                    priceCompletableFuture,
                    baseAttrCompletableFuture,
                    categoryCompletableFuture,
                    tradeMarkCompletableFuture
            ).join();

            //6.调用ES的Java客户端将文档对象Goods存入索引库
            //6.1 创建文档创建请求对象 封装 操作索引库名称
            IndexRequest indexRequest = new IndexRequest(index_name);
            //6.2 设置文档ID  ES文档ID属性: _id
            indexRequest.id(skuId.toString());
            //6.3 设置请求参数中JSON请求体 封装到请求对象中
            String goodsJSONStr = JSON.toJSONString(goods);
            indexRequest.source(goodsJSONStr, XContentType.JSON);
            //6.4 执行新增文档请求- 发起http请求
            restHighLevelClient.index(indexRequest, RequestOptions.DEFAULT);
        } catch (Exception e) {
            log.error("[搜索服务]上架商品异常:{}", e);
            throw new RuntimeException(e);
        }
    }

    /**
     * 将指定商品文档删除
     *
     * @param skuId
     */
    @Override
    public void lowerGoods(Long skuId) {
        try {
            DeleteRequest request = new DeleteRequest(
                    index_name,
                    skuId.toString());
            restHighLevelClient.delete(request, RequestOptions.DEFAULT);
        } catch (IOException e) {
            log.error("[搜索服务]商品下架异常:{}", e);
            throw new RuntimeException(e);
        }
    }

    /**
     * 更新商品热度分值
     * 1.先操作Redis中热点商品排行榜
     * 2.稀释写ES次数
     *
     * @param skuId
     */
    @Override
    public void incrHotScore(Long skuId) {
        try {
            //1.先操作Redis中热点商品排行榜
            String zsetKey = "hotScore";
            //2.对Zset元素进行+1操作 返回热度值
            Double aDouble = redisTemplate.opsForZSet().incrementScore(zsetKey, skuId.toString(), 1);
            //3.有条件对ES进行写入
            if (aDouble % 10 == 0) {
                //3.1 创建修改文档对象
                UpdateRequest request = new UpdateRequest(
                        index_name, skuId.toString());
                Goods goods = new Goods();
                goods.setHotScore(aDouble.longValue());
                request.doc(JSON.toJSONString(goods), XContentType.JSON);

                //3.2 执行修改
                restHighLevelClient.update(
                        request, RequestOptions.DEFAULT);
            }
        } catch (IOException e) {
            log.error("[搜索服务]更新商品热度异常:{}", e);
            throw new RuntimeException(e);
        }

    }


    /**
     * 实现对商品索引库检索
     * 开发三大步骤: 1.构建检索请求对象  2.执行检索  3.封装相应结果
     *
     * @param searchParam
     * @return
     */
    @Override
    public SearchResponseVo search(SearchParam searchParam) {
        try {
            //一.创建用于检索请求对象(封装检索商品DSL语句)
            SearchRequest searchRequest = this.buildDSL(searchParam);

            //二.调用ES的Restful接口实现检索
            SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);

            //三.处理响应结果,按照接口文档返回
            return this.parseResult(searchResponse, searchParam);
        } catch (Exception e) {
            log.error("[检索服务]执行检索异常:{}", e);
            throw new RuntimeException(e);
        }
    }

    /**
     * 封装DSL语句-检索请求地址,相关请求参数(请求体相关参数)
     *
     * @param searchParam
     * @return
     */
    @Override
    public SearchRequest buildDSL(SearchParam searchParam) {
        //1.创建SearchRequest对象,封装查询索引库名称 请求地址中索引库名称(请求地址以及方式)
        SearchRequest searchRequest = new SearchRequest(index_name);

        //2.创建SearchSourceBuilder,封装请求体JSON参数-包含:1.查询方式 2.过滤字段 3.分页 4.高亮 5.排序 6.聚合
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        //2.1 TODO 设置请求体参数中 query 部分:设置查询方式(关键字查询,过滤项过滤)
        //2.1.1 创建封装所有查询条件:bool布尔查询对象 ES中接口名称:XXXBuilder 一般都会提供XXXBuilders静态方法创建对应builder对象
        BoolQueryBuilder allBoolQueryBuilder = QueryBuilders.boolQuery();
        //2.1.2 在bool中通过must指定关键字查询
        if (StringUtils.isNotBlank(searchParam.getKeyword())) {
            allBoolQueryBuilder.must(QueryBuilders.matchQuery("title", searchParam.getKeyword()).operator(Operator.AND));
        }
        //2.1.3 在bool中通过filter指定品牌过滤,三级分类过滤,平台属性过滤
        //2.1.3.1 判断是否有品牌过滤条件,如果有设置品牌ID过滤 提交参数名:trademark 参数值:  品牌ID:品牌名称
        if (StringUtils.isNotBlank(searchParam.getTrademark())) {
            String[] split = searchParam.getTrademark().split(":");
            if (split != null && split.length == 2) {
                allBoolQueryBuilder.filter(QueryBuilders.termQuery("tmId", split[0]));
            }
        }
        //2.1.3.2 判断是否有三级分类过滤条件,如果有设置分类ID过滤
        if (searchParam.getCategory1Id() != null) {
            allBoolQueryBuilder.filter(QueryBuilders.termQuery("category1Id", searchParam.getCategory1Id()));
        }
        if (searchParam.getCategory2Id() != null) {
            allBoolQueryBuilder.filter(QueryBuilders.termQuery("category2Id", searchParam.getCategory2Id()));
        }
        if (searchParam.getCategory3Id() != null) {
            allBoolQueryBuilder.filter(QueryBuilders.termQuery("category3Id", searchParam.getCategory3Id()));
        }
        //2.1.3.3 判断是否有平台属性过滤条件,如果有设置平台属性过滤 数组每个参数形式:  平台属性ID:平台属性值:平台属性名称
        if (searchParam.getProps() != null && searchParam.getProps().length > 0) {
            for (String prop : searchParam.getProps()) {
                //每遍历一次都需要构建nested嵌套bool布尔查询
                String[] split = prop.split(":");
                if (split != null && split.length == 3) {
                    //每个平台属性过滤条件,都是一组bool查询 .构建当前平台属性bool条件
                    BoolQueryBuilder attrBoolQueryBuilder = QueryBuilders.boolQuery();
                    attrBoolQueryBuilder.must(QueryBuilders.termQuery("attrs.attrId", split[0]));
                    attrBoolQueryBuilder.must(QueryBuilders.termQuery("attrs.attrValue", split[1]));
                    NestedQueryBuilder nestedQueryBuilder = QueryBuilders.nestedQuery("attrs", attrBoolQueryBuilder, ScoreMode.None);
                    //将每个平台属性nested查询封装到总查询对象中
                    allBoolQueryBuilder.filter(nestedQueryBuilder);
                }
            }
        }
        searchSourceBuilder.query(allBoolQueryBuilder);
        //2.2 设置请求体参数中 _source 部分:选择查询字段响应字段
        searchSourceBuilder.fetchSource(new String[]{"id", "title", "defaultImg", "price"}, null);
        //2.3 设置请求体参数中 from size 部分:分页参数
        Integer pageNo = searchParam.getPageNo();
        Integer pageSize = searchParam.getPageSize();
        int from = (pageNo - 1) * pageSize;
        searchSourceBuilder.from(from).size(pageSize);
        //2.4 设置请求体参数中 highlight 部分:设置高亮文本片段 高亮三要素:1.高亮前置标签 2.高亮后置标签  3.高亮字段
        if (StringUtils.isNotBlank(searchParam.getKeyword())) {
            HighlightBuilder highlightBuilder = new HighlightBuilder();
            highlightBuilder.preTags("<font style='color:red'>");
            highlightBuilder.postTags("</font>");
            highlightBuilder.field("title");
            searchSourceBuilder.highlighter(highlightBuilder);
        }
        //2.5 设置请求体参数中 sort 部分:设置排序字段 参数名称:order  参数值形式 1(综合)|2(价格):asc(升序)|des(降序)  例如: 1:asc
        if (StringUtils.isNotBlank(searchParam.getOrder())) {
            String[] split = searchParam.getOrder().split(":");
            //前端排序参数值 必须遵从规范
            String orderField = "";
            if (split != null && split.length == 2) {
                if ("1".equals(split[0])) {
                    orderField = "hotScore";
                }
                if ("2".equals(split[0])) {
                    orderField = "price";
                }
                searchSourceBuilder.sort(orderField, "asc".equals(split[1]) ? SortOrder.ASC : SortOrder.DESC);
            }
        }
        //2.6 TODO 设置请求体参数中 aggs 部分:设置排序聚合(品牌聚合,平台属性聚合)-动态聚合过滤条件
        //2.6.1 对品牌ID进行聚合,在品牌聚合内部增加品牌名称子聚合以及品牌Logo子聚合
        //2.6.1.1 创建品牌ID聚合对象 聚合三要素:1.聚合名称 2.聚合类型 3.聚合字段  terms("聚合名称")聚合类型-将相同属性值放在一组
        TermsAggregationBuilder tmIdAgg = AggregationBuilders.terms("tmIdAgg").field("tmId");
        //2.6.1.2 基于品牌ID聚合对象,创建品牌名称子聚合对象
        tmIdAgg.subAggregation(AggregationBuilders.terms("tmNameAgg").field("tmName"));
        //2.6.1.3 基于品牌ID聚合对象,创建品牌Logo子聚合对象
        tmIdAgg.subAggregation(AggregationBuilders.terms("tmLogoAgg").field("tmLogoUrl"));
        //2.6.1.4 将品牌ID聚合加入"aggs"内部
        searchSourceBuilder.aggregation(tmIdAgg);

        //2.6.2 对平台属性进行聚合
        //2.6.2.1 创建"嵌套"类型字段:attrs平台属性聚合对象
        NestedAggregationBuilder attrsNestedAgg = AggregationBuilders.nested("attrsAgg", "attrs");
        //2.6.2.2 基于平台属性聚合对象创建平台属性ID子聚合对象
        TermsAggregationBuilder attrsIdAgg = AggregationBuilders.terms("attrsIdAgg").field("attrs.attrId");
        //2.6.2.3 基于平台属性ID子聚合对象创建平台属性名称子聚合对象
        attrsIdAgg.subAggregation(AggregationBuilders.terms("attrsNameAgg").field("attrs.attrName"));
        //2.6.2.4 基于平台属性ID子聚合对象创建平台属性值子聚合对象
        attrsIdAgg.subAggregation(AggregationBuilders.terms("attrsValueAgg").field("attrs.attrValue"));
        //2.6.2.5 将平台属性ID聚合加入到平台属性聚合内部
        attrsNestedAgg.subAggregation(attrsIdAgg);
        //2.6.2.5 将平台属性聚合加入"aggs"内部
        searchSourceBuilder.aggregation(attrsNestedAgg);

        System.err.println(searchSourceBuilder.toString());
        System.err.println("---------------------------------------");
        //3.将请求对象跟请求体对象进行绑定
        return searchRequest.source(searchSourceBuilder);
    }


    /**
     * 从ES响应结果JSON中解析检索商品索引库结果
     *
     * @param searchResponse
     * @param searchParam
     * @return
     */
    @Override
    public SearchResponseVo parseResult(SearchResponse searchResponse, SearchParam searchParam) {
        SearchResponseVo vo = new SearchResponseVo();
        //1.从ES响应对象中获取分页相关数据,封装VO中分页信息
        vo.setPageNo(searchParam.getPageNo());
        Integer pageSize = searchParam.getPageSize();
        vo.setPageSize(pageSize);
        //1.1 获取命中总记录数
        long total = searchResponse.getHits().getTotalHits().value;
        vo.setTotal(total);
        //1.2 计算总页数
        // 不能整除- total:11  pageSize:5  `totalPages= (total/pageSize)+1
        //  能整除- total:12  pageSize:5  totalPages= total/pageSize
        long totalPages = total % pageSize == 0 ? total / pageSize : total / pageSize + 1;
        vo.setTotalPages(totalPages);

        //2.获取检索到业务数据(注意高亮),封装Vo中商品列表
        SearchHit[] hits = searchResponse.getHits().getHits();
        if (hits != null && hits.length > 0) {
            //2.1 将命中搜索对象数组转为集合
            List<SearchHit> searchHitList = Arrays.asList(hits);
            //2.2 遍历集合封装为商品文档对象Goods
            List<Goods> goodsList = searchHitList.stream().map(searchHit -> {
                //2.2.1 获取命中检索对象中JSON结果
                String goodsString = searchHit.getSourceAsString();
                //2.2.2 将JSON转为Goods对象
                Goods goods = JSON.parseObject(goodsString, Goods.class);
                //2.2.3 处理高亮
                Map<String, HighlightField> highlightFields = searchHit.getHighlightFields();
                if (!CollectionUtils.isEmpty(highlightFields)) {
                    //2.3.1 获取标题高亮结果
                    HighlightField highlightField = highlightFields.get("title");
                    //2.3.2 获取高亮文本片段
                    String highlightTitle = highlightField.getFragments()[0].toString();
                    goods.setTitle(highlightTitle);
                }
                return goods;
            }).collect(Collectors.toList());
            vo.setGoodsList(goodsList);
        }
        //3.获取聚合到品牌结果,封装Vo中用于动态展示过滤项条件-品牌
        //3.1 获取到所有聚合结果将结果转为Map
        Map<String, Aggregation> allAggregationMap = searchResponse.getAggregations().asMap();
        //3.2 获取品牌ID聚合结果对象
        ParsedLongTerms tmIdAgg = (ParsedLongTerms) allAggregationMap.get("tmIdAgg");
        //3.3 遍历品牌聚合结果桶数组获取到品牌ID
        List<SearchResponseTmVo> tmVoList = tmIdAgg.getBuckets().stream().map(bucket -> {
            //3.3.1 获取品牌ID
            long tmId = bucket.getKeyAsNumber().longValue();
            //3.3.1 获取到品牌ID子聚合结果转为Map
            Map<String, Aggregation> tmIdSubAggregationMap = bucket.getAggregations().asMap();
            //3.3.2 通过品牌ID聚合结果获取品牌名称子聚合结果 获取到聚合名称
            ParsedStringTerms tmNameAgg = (ParsedStringTerms) tmIdSubAggregationMap.get("tmNameAgg");
            String tmName = tmNameAgg.getBuckets().get(0).getKeyAsString();

            //3.3.3 通过品牌ID聚合结果获取品牌Logo子聚合结果 获取到聚合品牌值
            ParsedStringTerms tmLogoAgg = (ParsedStringTerms) tmIdSubAggregationMap.get("tmLogoAgg");
            String tmLogo = tmLogoAgg.getBuckets().get(0).getKeyAsString();

            //3.3.4 创建品牌聚合结果对象
            SearchResponseTmVo tmVo = new SearchResponseTmVo();
            tmVo.setTmId(tmId);
            tmVo.setTmName(tmName);
            tmVo.setTmLogoUrl(tmLogo);
            return tmVo;
        }).collect(Collectors.toList());
        vo.setTrademarkList(tmVoList);

        //4.获取聚合到平台属性结果,封装Vo中用于动态展示过滤项条件-平台属性
        //4.1 获取平台属性聚合结果对象
        ParsedNested attrsAgg = (ParsedNested) allAggregationMap.get("attrsAgg");
        //4.2 从平台属性聚合结果对象获取平台属性attrsIdAgg子聚合
        ParsedLongTerms attrsIdAgg = attrsAgg.getAggregations().get("attrsIdAgg");
        //遍历平台属性ID聚合结果中"桶"集合,没遍历一次得到一条平台属性过滤条件
        if (!CollectionUtils.isEmpty(attrsIdAgg.getBuckets())) {
            List<SearchResponseAttrVo> attrVoList = attrsIdAgg.getBuckets().stream().map(bucket -> {
                //4.3 创建聚合平台属性vo对象
                SearchResponseAttrVo attrVo = new SearchResponseAttrVo();
                //4.2.0 获取平台属性ID
                long attrId = bucket.getKeyAsNumber().longValue();
                attrVo.setAttrId(attrId);
                //4.2.1 从平台属性attrsIdAgg子聚合对象获取平台属性名称子聚合
                ParsedStringTerms attrsNameAgg = bucket.getAggregations().get("attrsNameAgg");
                if (!CollectionUtils.isEmpty(attrsNameAgg.getBuckets())) {
                    String attrName = attrsNameAgg.getBuckets().get(0).getKeyAsString();
                    attrVo.setAttrName(attrName);
                }
                //4.2.2 从平台属性attrsIdAgg子聚合对象获取平台属性值子聚合
                ParsedStringTerms attrsValueAgg = bucket.getAggregations().get("attrsValueAgg");
                if (!CollectionUtils.isEmpty(attrsValueAgg.getBuckets())) {
                    //遍历当前平台属性包含属性值集合
                    List<String> attrValueList = attrsValueAgg.getBuckets().stream().map(attrsValueBucket -> {
                        return attrsValueBucket.getKeyAsString();
                    }).collect(Collectors.toList());
                    attrVo.setAttrValueList(attrValueList);
                }
                return attrVo;
            }).collect(Collectors.toList());
            vo.setAttrsList(attrVoList);
        }
        return vo;
    }
}
